package org.xbdframework.boot;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.*;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.GenericApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.util.ObjectUtils;

/**
 * <p>
 * 所有的事件按执行的先后顺序如下：
 * <ul>
 * <li>ApplicationStartingEvent：在Spring最开始启动的时候触发</li>
 * <li>ApplicationEnvironmentPreparedEvent：在Spring已经准备好上下文但是上下文尚未创建的时候触发</li>
 * <li>ApplicationPreparedEvent：在Bean定义加载之后、刷新上下文之前触发</li>
 * <li>ApplicationStartedEvent（Spring Boot 2.0新增的事件）：在刷新上下文之后、调用application命令之前触发</li>
 * <li>ApplicationReadyEvent：在调用application命令之后触发</li>
 * <li>ApplicationFailedEvent：在启动Spring发生异常时触发</li>
 * </ul>
 *
 * <p>
 * 在Spring Boot 2.0中对事件模型做了一些增强，主要就是增加了ApplicationStartedEvent事件.
 * </p>
 * <p>
 * 官方文档对ApplicationStartedEvent和ApplicationReadyEvent的解释如下：<br>
 * <pre>
 *  An ApplicationStartedEvent is sent after the context has been refreshed but before any application and command-line runners have been called.
 *  An ApplicationReadyEvent is sent after any application and command-line runners have been called. It indicates that the application is ready to service requests
 * </pre>
 *
 * @author luas
 * @since 1.5.0
 */
public class XbdApplicationStartupTraceListener implements GenericApplicationListener {

    /**
     * The default order for the LoggingApplicationListener.
     */
    public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 21;

    public static final String APPLICATION_STARTUP_PREFIX_CONFIG = "xbd.application.startup.trace.";

    public static final String TRACE_ENABLE_CONFIG = "enable";

    public static final String ENVIRONMENT_PRINTABLE_CONFIG = "environment.printable";

    private static final Class<?>[] EVENT_TYPES = { ApplicationStartingEvent.class,
            ApplicationEnvironmentPreparedEvent.class, ApplicationPreparedEvent.class,
            ApplicationFailedEvent.class, ApplicationStartedEvent.class,
            ApplicationReadyEvent.class };

    private static final Class<?>[] SOURCE_TYPES = { SpringApplication.class,
            ApplicationContext.class };

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private int order = DEFAULT_ORDER;

    private LocalDateTime startTime;

    private LocalDateTime readyTime;

    private Boolean traceEnable;

    private Boolean envPrintable;

    @Override
    public boolean supportsEventType(ResolvableType eventType) {
        return isAssignableFrom(eventType.getRawClass(), EVENT_TYPES);
    }

    @Override
    public boolean supportsSourceType(Class<?> sourceType) {
        return isAssignableFrom(sourceType, SOURCE_TYPES);
    }

    private boolean isAssignableFrom(Class<?> type, Class<?>... supportedTypes) {
        if (type != null) {
            for (Class<?> supportedType : supportedTypes) {
                if (supportedType.isAssignableFrom(type)) {
                    return true;
                }
            }
        }

        return false;
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationStartingEvent) {
            onApplicationStartingEvent((ApplicationStartingEvent) event);
        }
        else if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent(
                    (ApplicationEnvironmentPreparedEvent) event);
        }
        else if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent((ApplicationPreparedEvent) event);
        }
        else if (event instanceof ApplicationFailedEvent) {
            onApplicationFailedEvent((ApplicationFailedEvent) event);
        }
        else if (event instanceof ApplicationStartedEvent) {
            onApplicationStartedEvent((ApplicationStartedEvent) event);
        }
        else if (event instanceof ApplicationReadyEvent) {
            onApplicationReadyEvent((ApplicationReadyEvent) event);
        }
    }

    private void onApplicationStartingEvent(ApplicationStartingEvent event) {
        this.startTime = LocalDateTime.now();
        System.out.println("application starting on " + this.startTime + ".");
    }

    private void onApplicationEnvironmentPreparedEvent(
            ApplicationEnvironmentPreparedEvent event) {
        Binder binder = Binder.get(event.getEnvironment());

        traceEnable = binder.bind(APPLICATION_STARTUP_PREFIX_CONFIG + TRACE_ENABLE_CONFIG,
                Boolean.class).orElse(false);
        envPrintable = binder
                .bind(APPLICATION_STARTUP_PREFIX_CONFIG + ENVIRONMENT_PRINTABLE_CONFIG,
                        Boolean.class)
                .orElse(false);

        if (isTraceEnable()) {
            this.logger.info(
                    "application environment prepared on {}, default profile is {}, current active profile is {}.",
                    new Object[] { LocalDateTime.now(),
                            event.getEnvironment().getDefaultProfiles(),
                            event.getEnvironment().getActiveProfiles() });

            if (isEnvPrintable()) {
                Map<String, Object> systemProperties = event.getEnvironment()
                        .getSystemProperties();

                if (!ObjectUtils.isEmpty(systemProperties)) {
                    for (Map.Entry<String, Object> property : systemProperties
                            .entrySet()) {
                        this.logger.info("environment item {}={}.",
                                new Object[] { property.getKey(), property.getValue() });
                    }
                }
            }
        }
    }

    private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
        if (isTraceEnable()) {
            this.logger.info("application prepared on {}.",
                    new Object[] { LocalDateTime.now() });
        }
    }

    private void onApplicationStartedEvent(ApplicationStartedEvent event) {
        if (isTraceEnable()) {
            this.logger.info("application started on {}...",
                    new Object[] { LocalDateTime.now() });
        }
    }

    private void onApplicationReadyEvent(ApplicationReadyEvent event) {
        this.readyTime = LocalDateTime.now();

        System.out.println("application ready on " + this.readyTime + ".");

        if (isTraceEnable()) {
            this.logger.info("application starting on {}, ready on {}, taking {} ms.",
                    new Object[] { this.startTime, this.readyTime, takingTime() });
        }
    }

    private void onApplicationFailedEvent(ApplicationFailedEvent event) {
        if (isTraceEnable()) {
            this.logger.info("application failed on {}, the exception details is {}.",
                    new Object[] { LocalDateTime.now(), event.getException() });
        }
    }

    @Override
    public int getOrder() {
        return this.order;
    }

    private boolean isTraceEnable() {
        return this.traceEnable;
    }

    private boolean isEnvPrintable() {
        return this.envPrintable;
    }

    private long takingTime() {
        return Duration.between(this.startTime, this.readyTime).toMillis();
    }

}
